package org.jeecg.modules.cas.util;

import lombok.extern.slf4j.Slf4j;
import org.w3c.dom.Document;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.StringReader;
import java.util.*;

/**
 * 解析cas,ST验证后的xml
 *
 */
@Slf4j
public final class XmlUtils {

  /**
   * Creates a new namespace-aware DOM document object by parsing the given XML.
   *
   * @param xml XML content.
   *
   * @return DOM document.
   */
  public static Document newDocument(final String xml) {
    final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    final Map<String, Boolean> features = new HashMap<String, Boolean>();
    features.put(XMLConstants.FEATURE_SECURE_PROCESSING, true);
    features.put("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
    for (final Map.Entry<String, Boolean> entry : features.entrySet()) {
      try {
        factory.setFeature(entry.getKey(), entry.getValue());
      } catch (ParserConfigurationException e) {
        log.warn("Failed setting XML feature {}: {}", entry.getKey(), e);
      }
    }
    factory.setNamespaceAware(true);
    try {
      return factory.newDocumentBuilder().parse(new InputSource(new StringReader(xml)));
    } catch (Exception e) {
      throw new RuntimeException("XML parsing error: " + e);
    }
  }

  /**
   * Get an instance of an XML reader from the XMLReaderFactory.
   *
   * @return the XMLReader.
   */
  public static XMLReader getXmlReader() {
    try {
      final XMLReader reader = SAXParserFactory.newInstance().newSAXParser().getXMLReader();
      reader.setFeature("http://xml.org/sax/features/namespaces", true);
      reader.setFeature("http://xml.org/sax/features/namespace-prefixes", false);
      reader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
      return reader;
    } catch (final Exception e) {
      throw new RuntimeException("Unable to create XMLReader", e);
    }
  }

  /**
   * Retrieve the text for a group of elements. Each text element is an entry
   * in a list.
   * <p>
   * This method is currently optimized for the use case of two elements in a
   * list.
   *
   * @param xmlAsString the xml response
   * @param element     the element to look for
   * @return the list of text from the elements.
   */
  public static List<String> getTextForElements(final String xmlAsString, final String element) {
    final List<String> elements = new ArrayList<String>(2);
    final XMLReader reader = getXmlReader();

    final DefaultHandler handler = new DefaultHandler() {

      private boolean foundElement = false;

      private StringBuilder buffer = new StringBuilder();

      public void startElement(final String uri, final String localName, final String qName,
          final Attributes attributes) throws SAXException {
        if (localName.equals(element)) {
          this.foundElement = true;
        }
      }

      public void endElement(final String uri, final String localName, final String qName) throws SAXException {
        if (localName.equals(element)) {
          this.foundElement = false;
          elements.add(this.buffer.toString());
          this.buffer = new StringBuilder();
        }
      }

      public void characters(char[] ch, int start, int length) throws SAXException {
        if (this.foundElement) {
          this.buffer.append(ch, start, length);
        }
      }
    };

    reader.setContentHandler(handler);
    reader.setErrorHandler(handler);

    try {
      reader.parse(new InputSource(new StringReader(xmlAsString)));
    } catch (final Exception e) {
      log.error(e.getMessage(), e);
      return null;
    }

    return elements;
  }

  /**
   * Retrieve the text for a specific element (when we know there is only
   * one).
   *
   * @param xmlAsString the xml response
   * @param element     the element to look for
   * @return the text value of the element.
   */
  public static String getTextForElement(final String xmlAsString, final String element) {
    final XMLReader reader = getXmlReader();
    final StringBuilder builder = new StringBuilder();

    final DefaultHandler handler = new DefaultHandler() {

      private boolean foundElement = false;

      public void startElement(final String uri, final String localName, final String qName,
          final Attributes attributes) throws SAXException {
        if (localName.equals(element)) {
          this.foundElement = true;
        }
      }

      public void endElement(final String uri, final String localName, final String qName) throws SAXException {
        if (localName.equals(element)) {
          this.foundElement = false;
        }
      }

      public void characters(char[] ch, int start, int length) throws SAXException {
        if (this.foundElement) {
          builder.append(ch, start, length);
        }
      }
    };

    reader.setContentHandler(handler);
    reader.setErrorHandler(handler);

    try {
      reader.parse(new InputSource(new StringReader(xmlAsString)));
    } catch (final Exception e) {
      log.error(e.getMessage(), e);
      return null;
    }

    return builder.toString();
  }

  public static Map<String, Object> extractCustomAttributes(final String xml) {
    final SAXParserFactory spf = SAXParserFactory.newInstance();
    spf.setNamespaceAware(true);
    spf.setValidating(false);
    try {
      final SAXParser saxParser = spf.newSAXParser();
      final XMLReader xmlReader = saxParser.getXMLReader();
      final CustomAttributeHandler handler = new CustomAttributeHandler();
      xmlReader.setContentHandler(handler);
      xmlReader.parse(new InputSource(new StringReader(xml)));
      return handler.getAttributes();
    } catch (final Exception e) {
      log.error(e.getMessage(), e);
      return Collections.emptyMap();
    }
  }

  private static class CustomAttributeHandler extends DefaultHandler {

    private Map<String, Object> attributes;

    private boolean foundAttributes;

    private String currentAttribute;

    private StringBuilder value;

    @Override
    public void startDocument() throws SAXException {
      this.attributes = new HashMap<String, Object>();
    }

    @Override
    public void startElement(final String namespaceURI, final String localName, final String qName,
        final Attributes attributes) throws SAXException {
      if ("attributes".equals(localName)) {
        this.foundAttributes = true;
      } else if (this.foundAttributes) {
        this.value = new StringBuilder();
        this.currentAttribute = localName;
      }
    }

    @Override
    public void characters(final char[] chars, final int start, final int length) throws SAXException {
      if (this.currentAttribute != null) {
        value.append(chars, start, length);
      }
    }

    @Override
    public void endElement(final String namespaceURI, final String localName, final String qName)
        throws SAXException {
      if ("attributes".equals(localName)) {
        this.foundAttributes = false;
        this.currentAttribute = null;
      } else if (this.foundAttributes) {
        final Object o = this.attributes.get(this.currentAttribute);

        if (o == null) {
          this.attributes.put(this.currentAttribute, this.value.toString());
        } else {
          final List<Object> items;
          if (o instanceof List) {
            items = (List<Object>) o;
          } else {
            items = new LinkedList<Object>();
            items.add(o);
            this.attributes.put(this.currentAttribute, items);
          }
          items.add(this.value.toString());
        }
      }
    }

    public Map<String, Object> getAttributes() {
      return this.attributes;
    }
  }

  public static void main(String[] args) {
    String result = "<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>\r\n" +
        "    <cas:authenticationSuccess>\r\n" +
        "        <cas:user>admin</cas:user>\r\n" +
        "        <cas:attributes>\r\n" +
        "            <cas:credentialType>UsernamePasswordCredential</cas:credentialType>\r\n" +
        "            <cas:isFromNewLogin>true</cas:isFromNewLogin>\r\n" +
        "            <cas:authenticationDate>2019-08-01T19:33:21.527+08:00[Asia/Shanghai]</cas:authenticationDate>\r\n"
        +
        "            <cas:authenticationMethod>RestAuthenticationHandler</cas:authenticationMethod>\r\n" +
        "            <cas:successfulAuthenticationHandlers>RestAuthenticationHandler</cas:successfulAuthenticationHandlers>\r\n"
        +
        "            <cas:longTermAuthenticationRequestTokenUsed>false</cas:longTermAuthenticationRequestTokenUsed>\r\n"
        +
        "        </cas:attributes>\r\n" +
        "    </cas:authenticationSuccess>\r\n" +
        "</cas:serviceResponse>";

    String errorRes = "<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>\r\n" +
        "    <cas:authenticationFailure code=\"INVALID_TICKET\">未能够识别出目标 &#39;ST-5-1g-9cNES6KXNRwq-GuRET103sm0-DESKTOP-VKLS8B3&#39;票根</cas:authenticationFailure>\r\n"
        +
        "</cas:serviceResponse>";

    String error = XmlUtils.getTextForElement(errorRes, "authenticationFailure");
    System.out.println("------" + error);

    String error2 = XmlUtils.getTextForElement(result, "authenticationFailure");
    System.out.println("------" + error2);
    String principal = XmlUtils.getTextForElement(result, "user");
    System.out.println("---principal---" + principal);
    Map<String, Object> attributes = XmlUtils.extractCustomAttributes(result);
    System.out.println("---attributes---" + attributes);
  }
}
